Skip to main content

Blotch Library

Blotch Library - our purpose-built toolkit designed to streamline Widget development.

It has many useful React components and hooks to help you create great Widgets quickly and easily. With a design system built in, specifically for E-ink displays, you can be sure your Widget will look great on any Blotch Smart Frame.

Why Use the Blotch Library?

The Blotch Library is designed to help you build beautiful, functional Widgets faster and with less code. Here are the key benefits:

E-ink Optimized

All components are specifically designed and optimized for E-ink displays, ensuring crisp visuals and excellent readability on your Blotch Smart Frame.

Developer Experience

Built with TypeScript and React, the library provides full type safety, autocomplete support, and a familiar development experience for modern web developers.

Ready-to-Use Components

Pre-built components for common Widget needs - charts, gauges, QR codes, and more - so you can focus on your Widget's unique features rather than reinventing the wheel.

Third-Party Integrations

Built-in OAuth support and integration helpers make it easy to connect your Widget to external services and APIs.

State Management

Simple, powerful hooks for managing state, storage, and data persistence across Frame updates.

Getting Started

info

For comprehensive, interactive documentation with live examples and code snippets, visit the Blotch Library Storybook.

  • Live component previews - See components in action
  • Interactive controls - Modify props and see real-time changes
  • Code examples - Copy-paste ready code snippets
  • Visual documentation - Playground and usage guidelines
  • TypeScript definitions - Full type information for all components and hooks

Importing Components and Hooks

To use any component or hook from the Blotch Library in the Widget Designer, import them using the ~lib/ prefix:

import { ComponentName } from "~lib/package-name";
import { useHookName } from "~lib/hook-name";
tip

The ~lib/ prefix is a special import path that works seamlessly in the Widget Designer environment.

Example Widget

Here's a simple example of a Widget using the Blotch Library:

import { useFetch } from "~lib/use-fetch";
import { BarIndicator } from "~lib/bar-indicator";

export const Widget = () => {
const { data, error, isLoading } = useFetch("https://api.example.com/stats");

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<div>
<h1>Stats Dashboard</h1>
<BarIndicator numerator={data.current} denominator={data.total} />
</div>
);
};

Available Components

Charts & Visualizations

BarIndicator

A horizontal progress bar perfect for showing completion percentages, progress, or ratios.

Import:

import { BarIndicator } from "~lib/bar-indicator";

Props:

PropTypeRequiredDescription
numeratornumberYesCurrent value
denominatornumberYesMaximum value
width${number}px | ${number}%NoWidth of the bar (default: "100%")

Example:

<BarIndicator numerator={75} denominator={100} width="200px" />

DonutGauge

A circular gauge that displays a ratio in an elegant donut chart format.

Import:

import { DonutGauge } from "~lib/donut-gauge";

Props:

PropTypeRequiredDescription
numeratornumberYesCurrent value
denominatornumberYesMaximum value

Example:

<DonutGauge numerator={7} denominator={10} />
info

The DonutGauge automatically adapts to light and dark modes for optimal E-ink display.

LineChart

A versatile line chart component for displaying trends and time-series data.

Import:

import { LineChart } from "~lib/line-chart";

Props:

PropTypeRequiredDescription
pointsnumber[] | [number, number][] | readonly [number, number][]YesData points to plot
axis{ y?: { name?: string; visible?: boolean }; x?: { name?: string; visible?: boolean } }NoAxis configuration

Example:

// Simple array of Y values
<LineChart points={[10, 20, 15, 30, 25]} />

// With X,Y coordinates
<LineChart
points={[[0, 10], [1, 20], [2, 15], [3, 30], [4, 25]]}
axis={{
x: { visible: true },
y: { visible: true }
}}
/>

Scannable Components

QRCode

Generate QR codes for URLs, text, or any scannable data.

Import:

import { QRCode } from "~lib/qr-code";

Props:

PropTypeRequiredDescription
textstringYesContent to encode in the QR code
sizenumberNoSize in pixels (default: 256)

Example:

<QRCode text="https://blotch.app" size={200} />
tip

Often useful if you want to link to a website or app from your Widget. For example if you want the user to read more information, or to get more context on their phone.

Available Hooks

Data Fetching

useFetch

tip

Use useFetch or useFetchProxy for data fetching instead of native fetch to benefit from built-in error handling, loading states and caching.

A powerful hook for making HTTP requests with support for all HTTP methods, retries, and error handling.

Import:

import { useFetch } from "~lib/use-fetch";

Usage:

const { data, error, isLoading } = useFetch<ResponseType>(url, config);

Parameters:

ParameterTypeRequiredDescription
urlstringYesThe URL to fetch data from
configRequestInitNoFetch configuration options

Return Value:

PropertyTypeDescription
dataTThe fetched data
errorErrorError object if request failed
isLoadingbooleanLoading state

Examples:

// GET request
const { data, error, isLoading } = useFetch("https://api.example.com/data");

// POST request with body
const { data, error, isLoading } = useFetch("https://api.example.com/data", {
method: "POST",
body: JSON.stringify({ key: "value" }),
headers: { "Content-Type": "application/json" },
});

// With retry logic
const { data, error, isLoading } = useFetch("https://api.example.com/data", {
retry: {
condition: (data) => data.status === "retry",
count: 5,
},
});
note

The fetch is executed when the component mounts and whenever the URL changes. If the URL is undefined or the component unmounts before data is retrieved, the fetch will not be executed.

useFetchProxy

A CORS-bypassing version of useFetch that uses a proxy to fetch data.

Import:

import { useFetchProxy } from "~lib/use-fetch-proxy";

Usage:

const { data, error, isLoading } = useFetchProxy<ResponseType>(url, config);
tip

Use useFetchProxy when you need to fetch data from APIs that don't allow cross-origin requests. It's a drop-in replacement for useFetch.

AI Integration

useAI

A simple hook for interacting with AI chat services.

Import:

import { useAI } from "~lib/use-ai";

Usage:

const { data, error } = useAI({ prompt: "Your prompt here" });

Parameters:

ParameterTypeRequiredDescription
promptstringYesThe text prompt to send to the AI

Return Value:

PropertyTypeDescription
dataAIResponseAI response with output
errorErrorError if request fails

Example:

const ChatWidget = () => {
const { data, error } = useAI({
prompt: "Write a haiku about coding",
});

if (error) return <div>Error: {error.message}</div>;
if (!data) return <div>Loading...</div>;

return <div>{data.output}</div>;
};

Location Services

useGeocode

Convert addresses into geographic coordinates (latitude and longitude).

Import:

import { useGeocode } from "~lib/geocoding";

Usage:

const geocodeData = useGeocode("New York, NY, US");

Return Value:

PropertyTypeDescription
zipstringPostal code
namestringLocation name
latnumberLatitude coordinate
lonnumberLongitude coordinate
countrystringCountry code

Example:

const LocationWidget = () => {
const geocodeData = useGeocode("San Francisco, CA, US");

if (!geocodeData) return <div>Loading...</div>;

return (
<div>
<h2>{geocodeData.name}</h2>
<p>
Coordinates: {geocodeData.lat}, {geocodeData.lon}
</p>
</div>
);
};
Address Formats

For best results, use these formats:

  • City, State, Country: "New York, NY, US"
  • City, Country: "London, UK"
  • Postal Code: "10001, US"

State & Storage

useStorage

Persist data across Frame updates.

Import:

import { useStorage } from "~lib/use-storage";

Usage:

const [data, setData] = useStorage<T>();

Example:

const Widget = () => {
const [data, setData] = useStorage<{ count: number }>();

const increment = () => {
setData({ count: (data?.count || 0) + 1 });
};

return (
<div>
<p>Count: {data?.count || 0}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
info

Data stored with useStorage persists between Widget updates, making it perfect for maintaining state like OAuth tokens, user preferences, or cached data.

UI State

useDarkMode

Detect the user's dark mode preference.

Import:

import { useDarkMode } from "~lib/use-dark-mode";

Usage:

const isDarkMode = useDarkMode();

Example:

const Widget = () => {
const isDarkMode = useDarkMode();

return (
<div
style={{
background: isDarkMode ? "#000" : "#fff",
color: isDarkMode ? "#fff" : "#000",
}}
>
<h1>{isDarkMode ? "🌙 Dark Mode" : "☀️ Light Mode"}</h1>
</div>
);
};

Third-Party Integrations

RequireIntegration

A component that conditionally renders content based on OAuth integration status.

Import:

import { RequireIntegration } from "~lib/integrations";

Props:

PropTypeRequiredDescription
integrationstringYesIntegration identifier
childrenReactNodeYesContent to render when authenticated

Example:

<RequireIntegration integration="auth.my-domain.com">
<div>You are authenticated!</div>
</RequireIntegration>

useIntegration

Access OAuth integration data within a RequireIntegration context.

Import:

import { useIntegration } from "~lib/integrations";

Usage:

const integration = useIntegration();

Return Value:

PropertyTypeDescription
hoststringThe hostname of the integration
accessTokenstring | undefinedOAuth access token
refreshTokenstring | undefinedOAuth refresh token
expiresInnumber | undefinedToken expiration time (seconds)
storedAtnumberTimestamp when token was stored
widgetUidstringUnique identifier for the widget
redirectUristringRedirect URI for authentication

Example:

<RequireIntegration integration="api.spotify.com">
{() => {
const integration = useIntegration();

const { data } = useFetch("https://api.spotify.com/v1/me", {
headers: {
Authorization: `Bearer ${integration.accessToken}`,
},
});

return <div>Welcome, {data?.display_name}!</div>;
}}
</RequireIntegration>
warning

useIntegration must be used within a component wrapped by RequireIntegration, otherwise it will return null.